﻿using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Core.Metadata.Edm;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Linq.Dynamic;
using EntityFrameworkETL.Extensions;

namespace EntityFrameworkETL.SaveBatchedChanges
{
    public interface ICommandDbContextSync
    {
        void Sync(DbContext context, IEnumerable<CommandCache> commandCaches);
    }

    public class CommandDbContextSync : ICommandDbContextSync
    {
        public void Sync(DbContext context, IEnumerable<CommandCache> commandCaches)
        {
            var typeNameDictionary = GetTypeNames(context);
            //Set TypeName
            commandCaches.ToList().ForEach(x => x.TypeName = typeNameDictionary[x.TableName]);
            //Find assembly
            var assembly = GetAssemblyForModels(commandCaches);
            foreach (var commandCache in commandCaches)
            {
                //Set type
                commandCache.Type = assembly.ExportedTypes.Single(x => x.Name == commandCache.TypeName);
                //Set Entity
                LoadMemoryEntities(context, commandCache);
                commandCache.PrimaryKeyColumnName = GetPrimaryKeyColumnName(context, commandCache.Type);
                commandCache.PrimaryKeyPropertyName = GetPrimaryKeyPropertyName(context, commandCache.Type);
            }

        }

        private static Dictionary<Type, string> PrimaryKeyColumnNameDictionary {get; set; }
        public string GetPrimaryKeyColumnName(DbContext context, Type type)
        {
            if (PrimaryKeyColumnNameDictionary==null)
                PrimaryKeyColumnNameDictionary = new Dictionary<Type, string>();

            if (PrimaryKeyColumnNameDictionary.Keys.Contains(type))
                return PrimaryKeyColumnNameDictionary[type];

            //find primarykeys
            var keys = context.GetTableKeyColumns(type);
            if (keys.Count != 1)
                throw new Exception("SaveBatchedChanges does not support tables with multiple primary keys.");

            PrimaryKeyColumnNameDictionary.Add(type, keys.First().Key);
            return PrimaryKeyColumnNameDictionary[type];

        }

        private static Dictionary<Type, string> PrimaryKeyPropertyNameDictionary { get; set; }
        public string GetPrimaryKeyPropertyName(DbContext context, Type type)
        {
            if (PrimaryKeyPropertyNameDictionary == null)
                PrimaryKeyPropertyNameDictionary = new Dictionary<Type, string>();

            if (PrimaryKeyPropertyNameDictionary.Keys.Contains(type))
                return PrimaryKeyPropertyNameDictionary[type];

            //find primarykeys
            var keys = context.GetIdProperties(type).ToList();
            if (keys.Count() != 1)
                throw new Exception("SaveBatchedChanges does not support entities with multiple primary keys.");

            PrimaryKeyPropertyNameDictionary.Add(type, keys.First().Name);
            return PrimaryKeyColumnNameDictionary[type];
        }
        public static Dictionary<string, string> GetTypeNames(DbContext context)
        {
            var tableNameDictionary = new Dictionary<string, string>();
            ObjectContext octx = (context as IObjectContextAdapter).ObjectContext;
            foreach (var set in octx.MetadataWorkspace.GetItemCollection(DataSpace.SSpace)
                .GetItems<EntityContainer>().Single().BaseEntitySets)
            {
                var typeName = set.Name;
                var tableName = String.Concat(set.MetadataProperties["Schema"].Value, ".", set.MetadataProperties["Table"].Value);
                if (tableName != ".")
                    tableNameDictionary.Add(tableName, typeName);
            }
            return tableNameDictionary;
        }

        public static Assembly GetAssemblyForModels(IEnumerable<CommandCache> commandsCaches)
        {
            var typeNames = commandsCaches.Select(x => x.TypeName).ToList();
            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                var exportedTypes = assembly.ExportedTypes.Select(x => x.Name);
                if (typeNames.TrueForAll(exportedTypes.Contains))
                {
                    return assembly;
                }
            }
            return null;
        }

        public static void LoadMemoryEntities(DbContext dbContext, CommandCache commandCache)
        {
            foreach (var o in dbContext.Set(commandCache.Type).Local.AsQueryable().Where("Id = @0", commandCache.FakeId))
            {
                commandCache.Entity = o;
            }
        }
    }
}
